package org.g4studio.core.orm.xibatis.support;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Properties;

import javax.sql.DataSource;

import org.g4studio.core.orm.xibatis.common.xml.NodeletException;
import org.g4studio.core.orm.xibatis.sqlmap.client.SqlMapClient;
import org.g4studio.core.orm.xibatis.sqlmap.client.SqlMapClientBuilder;
import org.g4studio.core.orm.xibatis.sqlmap.engine.builder.xml.SqlMapConfigParser;
import org.g4studio.core.orm.xibatis.sqlmap.engine.builder.xml.SqlMapParser;
import org.g4studio.core.orm.xibatis.sqlmap.engine.builder.xml.XmlParserState;
import org.g4studio.core.orm.xibatis.sqlmap.engine.impl.ExtendedSqlMapClient;
import org.g4studio.core.orm.xibatis.sqlmap.engine.transaction.TransactionConfig;
import org.g4studio.core.orm.xibatis.sqlmap.engine.transaction.TransactionManager;
import org.g4studio.core.orm.xibatis.sqlmap.engine.transaction.external.ExternalTransactionConfig;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.NestedIOException;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.util.ObjectUtils;

/**
 * {@link org.springframework.beans.factory.FactoryBean} that creates an way to
 * set up a shared iBATIS SqlMapClient in a Spring application context; the
 * SqlMapClient can then be passed to iBATIS-based DAOs via dependency
 * injection.
 * 
 * <p>
 * Either
 * {@link org.springframework.jdbc.datasource.DataSourceTransactionManager} or
 * {@link org.springframework.transaction.jta.JtaTransactionManager} can be used
 * for transaction demarcation in combination with a SqlMapClient, with JTA only
 * necessary for transactions which span multiple databases.
 * 
 * <p>
 * Allows for specifying a DataSource at the SqlMapClient level. This is
 * preferable to per-DAO DataSource references, as it allows for lazy loading
 * and avoids repeated DataSource references in every DAO.
 * 
 * <p>
 * <b>Note:</b> As of Spring 2.5.5, this class (finally) requires iBATIS 2.3 or
 * higher. The new "mappingLocations" feature requires iBATIS 2.3.2.
 * 
 * @author Juergen Hoeller
 * @since 24.02.2004
 * @see #setConfigLocation
 * @see #setDataSource
 * @see SqlMapClientTemplate#setSqlMapClient
 * @see SqlMapClientTemplate#setDataSource
 */
public class SqlMapClientFactoryBean implements FactoryBean, InitializingBean {

	private static final ThreadLocal configTimeLobHandlerHolder = new ThreadLocal();

	/**
	 * Return the LobHandler for the currently configured iBATIS SqlMapClient,
	 * to be used by TypeHandler implementations like ClobStringTypeHandler.
	 * <p>
	 * This instance will be set before initialization of the corresponding
	 * SqlMapClient, and reset immediately afterwards. It is thus only available
	 * during configuration.
	 * 
	 * @see #setLobHandler
	 */
	public static LobHandler getConfigTimeLobHandler() {
		return (LobHandler) configTimeLobHandlerHolder.get();
	}

	private Resource[] configLocations;

	private Resource[] mappingLocations;

	private Properties sqlMapClientProperties;

	private DataSource dataSource;

	private boolean useTransactionAwareDataSource = true;

	private Class transactionConfigClass = ExternalTransactionConfig.class;

	private Properties transactionConfigProperties;

	private LobHandler lobHandler;

	private SqlMapClient sqlMapClient;

	public SqlMapClientFactoryBean() {
		this.transactionConfigProperties = new Properties();
		this.transactionConfigProperties.setProperty("SetAutoCommitAllowed", "false");
	}

	/**
	 * Set the location of the iBATIS SqlMapClient config file. A typical value
	 * is "WEB-INF/sql-map-config.xml".
	 * 
	 * @see #setConfigLocations
	 */
	public void setConfigLocation(Resource configLocation) {
		this.configLocations = (configLocation != null ? new Resource[] { configLocation } : null);
	}

	/**
	 * Set multiple locations of iBATIS SqlMapClient config files that are going
	 * to be merged into one unified configuration at runtime.
	 */
	public void setConfigLocations(Resource[] configLocations) {
		this.configLocations = configLocations;
	}

	/**
	 * Set locations of iBATIS sql-map mapping files that are going to be merged
	 * into the SqlMapClient configuration at runtime.
	 * <p>
	 * This is an alternative to specifying "&lt;sqlMap&gt;" entries in a
	 * sql-map-client config file. This property being based on Spring's
	 * resource abstraction also allows for specifying resource patterns here:
	 * e.g. "/myApp/*-map.xml".
	 * <p>
	 * Note that this feature requires iBATIS 2.3.2; it will not work with any
	 * previous iBATIS version.
	 */
	public void setMappingLocations(Resource[] mappingLocations) {
		this.mappingLocations = mappingLocations;
	}

	/**
	 * Set optional properties to be passed into the SqlMapClientBuilder, as
	 * alternative to a <code>&lt;properties&gt;</code> tag in the
	 * sql-map-config.xml file. Will be used to resolve placeholders in the
	 * config file.
	 * 
	 * @see #setConfigLocation
	 */
	public void setSqlMapClientProperties(Properties sqlMapClientProperties) {
		this.sqlMapClientProperties = sqlMapClientProperties;
	}

	/**
	 * Set the DataSource to be used by iBATIS SQL Maps. This will be passed to
	 * the SqlMapClient as part of a TransactionConfig instance.
	 * <p>
	 * If specified, this will override corresponding settings in the
	 * SqlMapClient properties. Usually, you will specify DataSource and
	 * transaction configuration <i>either</i> here <i>or</i> in SqlMapClient
	 * properties.
	 * <p>
	 * Specifying a DataSource for the SqlMapClient rather than for each
	 * individual DAO allows for lazy loading, for example when using
	 * PaginatedList results.
	 * <p>
	 * With a DataSource passed in here, you don't need to specify one for each
	 * DAO. Passing the SqlMapClient to the DAOs is enough, as it already
	 * carries a DataSource. Thus, it's recommended to specify the DataSource at
	 * this central location only.
	 * <p>
	 * Thanks to Brandon Goodin from the iBATIS team for the hint on how to make
	 * this work with Spring's integration strategy!
	 * 
	 * @see #setTransactionConfigClass
	 * @see #setTransactionConfigProperties
	 * @see SqlMapClientTemplate#setDataSource
	 * @see SqlMapClientTemplate#queryForPaginatedList
	 */
	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	/**
	 * Set whether to use a transaction-aware DataSource for the SqlMapClient,
	 * i.e. whether to automatically wrap the passed-in DataSource with Spring's
	 * TransactionAwareDataSourceProxy.
	 * <p>
	 * Default is "true": When the SqlMapClient performs direct database
	 * operations outside of Spring's SqlMapClientTemplate (for example, lazy
	 * loading or direct SqlMapClient access), it will still participate in
	 * active Spring-managed transactions.
	 * <p>
	 * As a further effect, using a transaction-aware DataSource will apply
	 * remaining transaction timeouts to all created JDBC Statements. This means
	 * that all operations performed by the SqlMapClient will automatically
	 * participate in Spring-managed transaction timeouts.
	 * <p>
	 * Turn this flag off to get raw DataSource handling, without Spring
	 * transaction checks. Operations on Spring's SqlMapClientTemplate will
	 * still detect Spring-managed transactions, but lazy loading or direct
	 * SqlMapClient access won't.
	 * 
	 * @see #setDataSource
	 * @see org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy
	 * @see org.springframework.jdbc.datasource.DataSourceTransactionManager
	 * @see SqlMapClientTemplate
	 */
	public void setUseTransactionAwareDataSource(boolean useTransactionAwareDataSource) {
		this.useTransactionAwareDataSource = useTransactionAwareDataSource;
	}

	/**
	 * Set the iBATIS TransactionConfig class to use. Default is
	 * <p>
	 * Will only get applied when using a Spring-managed DataSource. An instance
	 * of this class will get populated with the given DataSource and
	 * initialized with the given properties.
	 * <p>
	 * The default ExternalTransactionConfig is appropriate if there is external
	 * transaction management that the SqlMapClient should participate in: be it
	 * Spring transaction management, EJB CMT or plain JTA. This should be the
	 * typical scenario. If there is no active transaction, SqlMapClient
	 * operations will execute SQL statements non-transactionally.
	 * <p>
	 * JdbcTransactionConfig or JtaTransactionConfig is only necessary when
	 * using the iBATIS SqlMapTransactionManager API instead of external
	 * transactions. If there is no explicit transaction, SqlMapClient
	 * operations will automatically start a transaction for their own scope (in
	 * contrast to the external transaction mode, see above).
	 * <p>
	 * <b>It is strongly recommended to use iBATIS SQL Maps with Spring
	 * transaction management (or EJB CMT).</b> In this case, the default
	 * ExternalTransactionConfig is fine. Lazy loading and SQL Maps operations
	 * without explicit transaction demarcation will execute
	 * non-transactionally.
	 * <p>
	 * Even with Spring transaction management, it might be desirable to specify
	 * JdbcTransactionConfig: This will still participate in existing
	 * Spring-managed transactions, but lazy loading and operations without
	 * explicit transaction demaration will execute in their own auto-started
	 * transactions. However, this is usually not necessary.
	 * 
	 * @see #setDataSource
	 * @see #setTransactionConfigProperties
	 */
	public void setTransactionConfigClass(Class transactionConfigClass) {
		if (transactionConfigClass == null || !TransactionConfig.class.isAssignableFrom(transactionConfigClass)) {
			throw new IllegalArgumentException("Invalid transactionConfigClass: does not implement "
					+ "org.g4studio.core.orm.xibatis.sqlmap.engine.transaction.TransactionConfig");
		}
		this.transactionConfigClass = transactionConfigClass;
	}

	/**
	 * Set properties to be passed to the TransactionConfig instance used by
	 * this SqlMapClient. Supported properties depend on the concrete
	 * TransactionConfig implementation used:
	 * <p>
	 * <ul>
	 * <li><b>ExternalTransactionConfig</b> supports "DefaultAutoCommit"
	 * (default: false) and "SetAutoCommitAllowed" (default: true). Note that
	 * Spring uses SetAutoCommitAllowed = false as default, in contrast to the
	 * iBATIS default, to always keep the original autoCommit value as provided
	 * by the connection pool.
	 * <li><b>JdbcTransactionConfig</b> does not supported any properties.
	 * <li><b>JtaTransactionConfig</b> supports "UserTransaction" (no default),
	 * specifying the JNDI location of the JTA UserTransaction (usually
	 * "java:comp/UserTransaction").
	 * </ul>
	 */
	public void setTransactionConfigProperties(Properties transactionConfigProperties) {
		this.transactionConfigProperties = transactionConfigProperties;
	}

	/**
	 * Set the LobHandler to be used by the SqlMapClient. Will be exposed at
	 * config time for TypeHandler implementations.
	 * 
	 * @see #getConfigTimeLobHandler
	 */
	public void setLobHandler(LobHandler lobHandler) {
		this.lobHandler = lobHandler;
	}

	public void afterPropertiesSet() throws Exception {
		if (this.lobHandler != null) {
			// Make given LobHandler available for SqlMapClient configuration.
			// Do early because because mapping resource might refer to custom
			// types.
			configTimeLobHandlerHolder.set(this.lobHandler);
		}

		try {
			this.sqlMapClient = buildSqlMapClient(this.configLocations, this.mappingLocations,
					this.sqlMapClientProperties);

			// Tell the SqlMapClient to use the given DataSource, if any.
			if (this.dataSource != null) {
				TransactionConfig transactionConfig = (TransactionConfig) this.transactionConfigClass.newInstance();
				DataSource dataSourceToUse = this.dataSource;
				if (this.useTransactionAwareDataSource && !(this.dataSource instanceof TransactionAwareDataSourceProxy)) {
					dataSourceToUse = new TransactionAwareDataSourceProxy(this.dataSource);
				}
				transactionConfig.setDataSource(dataSourceToUse);
				transactionConfig.initialize(this.transactionConfigProperties);
				applyTransactionConfig(this.sqlMapClient, transactionConfig);
			}
		}

		finally {
			if (this.lobHandler != null) {
				// Reset LobHandler holder.
				configTimeLobHandlerHolder.set(null);
			}
		}
	}

	/**
	 * Build a SqlMapClient instance based on the given standard configuration.
	 * <p>
	 * The default implementation uses the standard iBATIS
	 * {@link SqlMapClientBuilder} API to build a SqlMapClient instance based on
	 * an InputStream (if possible, on iBATIS 2.3 and higher) or on a Reader (on
	 * iBATIS up to version 2.2).
	 * 
	 * @param configLocations
	 *            the config files to load from
	 * @param properties
	 *            the SqlMapClient properties (if any)
	 * @return the SqlMapClient instance (never <code>null</code>)
	 * @throws IOException
	 *             if loading the config file failed
	 */
	protected SqlMapClient buildSqlMapClient(Resource[] configLocations, Resource[] mappingLocations,
			Properties properties) throws IOException {

		if (ObjectUtils.isEmpty(configLocations)) {
			throw new IllegalArgumentException("At least 1 'configLocation' entry is required");
		}

		SqlMapClient client = null;
		SqlMapConfigParser configParser = new SqlMapConfigParser();
		for (int i = 0; i < configLocations.length; i++) {
			InputStream is = configLocations[i].getInputStream();
			try {
				client = configParser.parse(is, properties);
			} catch (RuntimeException ex) {
				throw new NestedIOException("Failed to parse config resource: " + configLocations[i], ex.getCause());
			}
		}

		if (mappingLocations != null) {
			SqlMapParser mapParser = SqlMapParserFactory.createSqlMapParser(configParser);
			for (int i = 0; i < mappingLocations.length; i++) {
				try {
					mapParser.parse(mappingLocations[i].getInputStream());
				} catch (NodeletException ex) {
					throw new NestedIOException("Failed to parse mapping resource: " + mappingLocations[i], ex);
				}
			}
		}

		return client;
	}

	/**
	 * Apply the given iBATIS TransactionConfig to the SqlMapClient.
	 * <p>
	 * The default implementation casts to ExtendedSqlMapClient, retrieves the
	 * maximum number of concurrent transactions from the
	 * SqlMapExecutorDelegate, and sets an iBATIS TransactionManager with the
	 * given TransactionConfig.
	 * 
	 * @param sqlMapClient
	 *            the SqlMapClient to apply the TransactionConfig to
	 * @param transactionConfig
	 *            the iBATIS TransactionConfig to apply
	 */
	protected void applyTransactionConfig(SqlMapClient sqlMapClient, TransactionConfig transactionConfig) {
		if (!(sqlMapClient instanceof ExtendedSqlMapClient)) {
			throw new IllegalArgumentException(
					"Cannot set TransactionConfig with DataSource for SqlMapClient if not of type "
							+ "ExtendedSqlMapClient: " + sqlMapClient);
		}
		ExtendedSqlMapClient extendedClient = (ExtendedSqlMapClient) sqlMapClient;
		transactionConfig.setMaximumConcurrentTransactions(extendedClient.getDelegate().getMaxTransactions());
		extendedClient.getDelegate().setTxManager(new TransactionManager(transactionConfig));
	}

	public Object getObject() {
		return this.sqlMapClient;
	}

	public Class getObjectType() {
		return (this.sqlMapClient != null ? this.sqlMapClient.getClass() : SqlMapClient.class);
	}

	public boolean isSingleton() {
		return true;
	}

	/**
	 * Inner class to avoid hard-coded iBATIS 2.3.2 dependency (XmlParserState
	 * class).
	 */
	private static class SqlMapParserFactory {

		public static SqlMapParser createSqlMapParser(SqlMapConfigParser configParser) {
			// Ideally: XmlParserState state = configParser.getState();
			// Should raise an enhancement request with iBATIS...
			XmlParserState state = null;
			try {
				Field stateField = SqlMapConfigParser.class.getDeclaredField("state");
				stateField.setAccessible(true);
				state = (XmlParserState) stateField.get(configParser);
			} catch (Exception ex) {
				throw new IllegalStateException(
						"iBATIS 2.3.2 'state' field not found in SqlMapConfigParser class - "
								+ "please upgrade to IBATIS 2.3.2 or higher in order to use the new 'mappingLocations' feature. "
								+ ex);
			}
			return new SqlMapParser(state);
		}
	}

}
